package io.prometheus.client.spring.web; import io.prometheus.client.Summary; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.context.annotation.Scope; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.web.bind.annotation.ControllerAdvice; import java.util.HashMap; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; /** * This class automatically times (via aspectj) the execution of annotated methods, if it's been enabled via {@link EnablePrometheusTiming}, * for methods annotated with {@link PrometheusTimeMethod} * * @author Andrew Stuart */ @Aspect("pertarget(io.prometheus.client.spring.web.MethodTimer.timeable())") @Scope("prototype") @ControllerAdvice public class MethodTimer { private final ReadWriteLock summaryLock = new ReentrantReadWriteLock(); private final HashMap<String, Summary> summaries = new HashMap<String, Summary>(); @Pointcut("@annotation(io.prometheus.client.spring.web.PrometheusTimeMethod)") public void annotatedMethod() {} @Pointcut("annotatedMethod()") public void timeable() {} private PrometheusTimeMethod getAnnotation(ProceedingJoinPoint pjp) throws NoSuchMethodException { assert(pjp.getSignature() instanceof MethodSignature); MethodSignature signature = (MethodSignature) pjp.getSignature(); PrometheusTimeMethod annot = AnnotationUtils.findAnnotation(pjp.getTarget().getClass(), PrometheusTimeMethod.class); if (annot != null) { return annot; } // When target is an AOP interface proxy but annotation is on class method (rather than Interface method). final String name = signature.getName(); final Class[] parameterTypes = signature.getParameterTypes(); return AnnotationUtils.findAnnotation(pjp.getTarget().getClass().getDeclaredMethod(name, parameterTypes), PrometheusTimeMethod.class); } private Summary ensureSummary(ProceedingJoinPoint pjp, String key) throws IllegalStateException { PrometheusTimeMethod annot; try { annot = getAnnotation(pjp); } catch (NoSuchMethodException e) { throw new IllegalStateException("Annotation could not be found for pjp \"" + pjp.toShortString() +"\"", e); } catch (NullPointerException e) { throw new IllegalStateException("Annotation could not be found for pjp \"" + pjp.toShortString() +"\"", e); } assert(annot != null); Summary summary; // We use a writeLock here to guarantee no concurrent reads. final Lock writeLock = summaryLock.writeLock(); writeLock.lock(); try { // Check one last time with full mutual exclusion in case multiple readers got null before creation. summary = summaries.get(key); if (summary != null) { return summary; } // Now we know for sure that we have never before registered. summary = Summary.build() .name(annot.name()) .help(annot.help()) .register(); // Even a rehash of the underlying table will not cause issues as we mutually exclude readers while we // perform our updates. summaries.put(key, summary); return summary; } finally { writeLock.unlock(); } } @Around("timeable()") public Object timeMethod(ProceedingJoinPoint pjp) throws Throwable { String key = pjp.getSignature().toLongString(); Summary summary; final Lock r = summaryLock.readLock(); r.lock(); try { summary = summaries.get(key); } finally { r.unlock(); } if (summary == null) { summary = ensureSummary(pjp, key); } final Summary.Timer t = summary.startTimer(); try { return pjp.proceed(); } finally { t.observeDuration(); } } }